/*
 Copyright (c) Whitemice Consulting, 2007, 2008, 2009
 Adam Tauno Williams <awilliam@whitemice.org>
 License: MIT/X11

 Permission is hereby granted, free of charge, to any person obtaining a copy
 of this software and associated documentation files (the "Software"), to deal
 in the Software without restriction, including without limitation the rights
 to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 copies of the Software, and to permit persons to whom the Software is
 furnished to do so, subject to the following conditions:

 The above copyright notice and this permission notice shall be included in
 all copies or substantial portions of the Software.
 
 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 THE SOFTWARE.
*/
using System;
using System.Collections;
using System.Collections.Generic;
using Gtk;
using Gdk;

namespace Whitemice.Calendar
{
  /* the daystrip widet draws a horizontal series of days and
     contains a collection of events which appear on those
     days. Days are CalendarEvent objects. */ 
  [System.ComponentModel.Category("Calendar")]
  [System.ComponentModel.ToolboxItem(true)]
  public class DayStrip : DrawingArea
  {
    protected enum DragMode { NoDrag, StretchStart, StetchAll, Appointment }
    protected int                         leftMargin, rightMargin;
    protected int                         padding, height, width;
    protected int                         increment, span = 7;
    protected DateTime                    firstDay = DateTime.Today;
    protected IList<CalendarDay>          dates;   
    protected IList<CalendarEvent>        events;
    protected double                      motionStartX, motionStartY;
    protected CalendarEvent               selectedEvent;
    protected DragMode                    dragMode;
    public    CalendarContextHandler      OnStripContext;
    public    CalendarEventContextHandler OnEventContext;
    public    CalendarEventDropHandler    OnEventDrop;
    
    public DayStrip() : base()
    {
      padding = 2; span = 7; firstDay = DateTime.Today;
      dates = new List<CalendarDay>();
      events = new List<CalendarEvent>();
      this.Events = Gdk.EventMask.AllEventsMask;
      dragMode = DragMode.NoDrag;
      ComputeDays();
    } /* end ctor */

    internal IList<CalendarDay> Dates
    {
      get { return dates; }
    }

    public IList<CalendarEvent> CalendarEvents 
    { 
      get { return events; } 
    }
    
    public void ClearEvents()
    {
      events.Clear();
    }

    public int Padding
    {
      get { return padding; }
      set { padding = value; }
    }

    public int Span 
    { 
      get { return span; }
      set 
      { 
        if (value != span)
        {
          span = value;
          ComputeDays();
        }
      }
    } /* end Span accessor */
    
    public DateTime FirstDay
    {
      get { return firstDay; }
      set 
      {
        if (value != firstDay)
        {
          firstDay = value;
          ComputeDays();
        }
      }
    } /* end FirstDay accessor */
    
    public DateTime LastDay
    {
      get { return firstDay.AddDays(dates.Count); }
    }
    
    internal bool SelectEvent(double _x, double _y)
    {
      return SelectEvent(Convert.ToInt32(_x), Convert.ToInt32(_y));
    }
    
    internal bool SelectEvent(int _x, int _y)
    {
      selectedEvent = SelectedEvent(_x, _y);
      if (selectedEvent == null)
        return false;
      return true;
    }
    
    internal CalendarEvent SelectedEvent(double _x, double _y)
    {
      return SelectedEvent(Convert.ToInt32(_x), Convert.ToInt32(_y)); 
    }
    
    internal CalendarEvent SelectedEvent(int _x, int _y)
    {
      CalendarEvent e;
      
      Console.WriteLine(" -- checking for day under pointer @ {0}x{1}--", _x, _y);
      e = null;
      foreach(CalendarEvent t in CalendarEvents)
        if (t.IsIndicated(_x, _y))
        {
          e = t;
          break;
        }
      if (e == null) Console.WriteLine("  -- no day under pointer --");
      return e;
    }
    
    protected bool CoordsToDateTime(ref double _x, ref double _y, out DateTime _i)
    {
      bool ok = false;
      
      _i = DateTime.Now;
      foreach(CalendarDay day in dates)
      {
        if (_x > day.Rectangle.X)
        {
          ok = true;
          _i = day.Date;
        } else break;
      }
      if (ok)
      {
        /* check y limits */
        if (_y > ((24 * 4) * increment))
          _y = ((24 * 4) * increment);
        /* check x limits */
        if (_x < leftMargin)
          _x = (leftMargin + 1);
        else if (_x > (Allocation.Width - rightMargin))
          _x = (Allocation.Width - rightMargin);
        /* calculate minutes into day */
        _i = _i.AddMinutes((_y / increment) * 15);
        return true;
      }
      return false;
    } /* end CoordsToDateTime */
    
    protected override bool OnExposeEvent (EventExpose ev)
    {  
      base.OnExposeEvent (ev);
      Paint (ev.Area);
      return true;
    } /* end OnExposeEvent */
    
    protected override bool OnMotionNotifyEvent (Gdk.EventMotion ev)
    {
      double   x, y;
      int      button;
      DateTime i;
      
      x = ev.X; y = ev.Y;
      if ((ev.State & Gdk.ModifierType.Button1Mask) == Gdk.ModifierType.Button1Mask)
        button = 1;
      else if ((ev.State & Gdk.ModifierType.Button2Mask) == Gdk.ModifierType.Button2Mask)
        button = 2;
      else if ((ev.State & Gdk.ModifierType.Button3Mask) == Gdk.ModifierType.Button3Mask)
        button = 3;
      else button = 0;
      if (CoordsToDateTime(ref x, ref y, out i))
      {
        switch(button)
        {
          case 1: DoLeftMotion(x, y, i); break;
          case 2: DoMiddleMotion(x, y, i); break;
          case 3: DoRightMotion(x, y, i); break;
          default: DoFreeMotion(x, y, i); break;
        }
      }
      return true;
    }
    
    //[GLib.ConnectBefore]
    protected override bool OnButtonPressEvent (Gdk.EventButton ev)
    {
      double   x, y;
      DateTime i;

      x = ev.X; y = ev.Y;
      if (CoordsToDateTime(ref x, ref y, out i))
      {
        motionStartX = x;
        motionStartY = y;
        switch (ev.Button)
        {
          case 1: DoLeftPress(x, y, i); break;
          case 2: DoMiddlePress(x, y, i); break;
          case 3: DoRightPress(x, y, i); break;
        }
      }
      return true;
    }
    
    protected override bool OnButtonReleaseEvent (Gdk.EventButton ev)
    {
      double   x, y;
      DateTime i;

      x = ev.X; y = ev.Y;
      if (CoordsToDateTime(ref x, ref y, out i))
      {
        switch (ev.Button)
        {
          case 1: DoLeftRelease(x, y, i); break;
          case 2: DoMiddleRelease(x, y, i); break;
          case 3: DoRightRelease(x, y, i); break;
        }
        motionStartX = -1.0;
        motionStartY = -1.0;
      }
      return true;
    }
    
    protected override void OnSizeAllocated (Gdk.Rectangle allocation)
    {
      base.OnSizeAllocated (allocation);
      ComputeDays();
    } /* OnSizeAllocated */
    
    protected override void OnSizeRequested (ref Requisition requisition)
    {
      requisition.Width = 800;
      requisition.Height = 1500;
    } /* end OnSizeRequested */
    
    public double PositionOfHour(int _hour)
    {
      return (increment * (_hour * 4.0));
    }

    protected virtual void ComputeMargins()
    {
      leftMargin = 40;
      if (Span > 1)
        rightMargin = 40;
      else rightMargin = 10;
      increment = ((Allocation.Height - (Padding * 2)) / (24 * 4));
      height = (increment * (24 * 4));
    } /* end ComputeMargins */

    protected virtual void ComputeDays()
    {
      CalendarDay day;
      
      if (span == 0)
        return;
      ComputeMargins();
      dates.Clear();
      width = ((Allocation.Width - (leftMargin + (Padding * 2 * Span) + rightMargin)) / Span);
      for (int i = 0; i < Span; i++) 
      {
  		  if (i == 0)
			  {
				  day = new CalendarDay(firstDay.AddDays(i),
			                          leftMargin + Padding,
			                          0,
			                          width,
			                          height);
        } else
          {
  			    day = new CalendarDay(firstDay.AddDays(i),
			                            (dates[(i - 1)].Rectangle.Right + (Padding * 2)),
			                            0,
			                            width,
			                            height);          
           }
        dates.Add(day);
		  } /* end for-days */
		  Repaint();
    } /* end ComputeDays */
    
    protected void Layout()
    {
      ComputeMargins();
    }
    
    public void Repaint()
    {
      this.QueueDraw();
    }
    
    protected void Paint(Gdk.Rectangle _rect)
    {
      Pango.Layout layout;
      int          h, w;
      
      Console.WriteLine("Paint(...)");
      if (this.IsDrawable)
      {
        /// ModifyBg(Gtk.StateType.Normal,new Gdk.Color(255,255,255));
        Style.PaintVline(Style, GdkWindow, StateType.Active,
                         _rect,this, null,
                         0, /* Y1 */
                         0, /* Y2 */
                         leftMargin /* X */);
        /* paint left margin of each day */
        foreach(CalendarDay day in dates)
          Style.PaintVline(Style, GdkWindow, StateType.Active,
                           _rect, this, null,
                           0, /* Y1 */ 
                           height, /* Y2 */
                           (day.Rectangle.Left) /* X */);
        /* paint right margin line */
        Style.PaintVline(Style, GdkWindow, StateType.Active,
                         _rect, this, null,
                         0, /* Y1 */
                         height, /* Y2 */
                         (Allocation.Width - rightMargin) /* X */);
        int offset, y;
        StateType stateType;
        /* Draw hours demarcs */
        for(int i = 0; i < ((24 * 4) + 1); i++)
        {
          stateType = StateType.Prelight;
          y = (increment * i);
          offset = 0;
          if ((i % 2) == 0)
          {
            offset = (leftMargin / 2);
            if ((i % 4) == 0)
            {
              /* Unit is divisble by four - this is an hour line */
              stateType = StateType.Normal;
              offset = leftMargin;
              /* don't draw hour lines for 0 hour or last hour */
              if ((i > 0) && (i < 96))
              {
                /* paint hour text */
                layout = new Pango.Layout(this.PangoContext);
                layout.FontDescription = Pango.FontDescription.FromString ("Tahoma 10");
                layout.SetText(string.Format("{0}:00", (i / 4)));
                layout.GetPixelSize(out w, out h);
                layout.Alignment = Pango.Alignment.Center;
                layout.Width = leftMargin;
                Style.PaintLayout(Style, GdkWindow, StateType.Normal, true, _rect, this, null, 
                                  (leftMargin / 2), /* X */ 
                                  (y - h), /* Y */ 
                                  layout);
                if (Span > 1)
                {
                  /* if Span > 1 then paint the hour names on the right side as well */
                  layout = new Pango.Layout(this.PangoContext);
                  layout.FontDescription = Pango.FontDescription.FromString ("Tahoma 10");
                  layout.SetText(string.Format("{0}:00", (i / 4)));
                  layout.Alignment = Pango.Alignment.Center;
                  layout.GetPixelSize(out w, out h);
                  layout.Width = leftMargin;
                  Style.PaintLayout(Style, GdkWindow, StateType.Normal, true, _rect, this, null, 
                                    (Allocation.Width - (rightMargin / 2)), /* X */
                                    (y - h), /* Y */
                                    layout);
                } /* end if Span > 1 */
              } /* end if hour-label-should-be-painted */
            } /* end if-interval-divisible-by-4 */
          } /* end if-interval-divisible-by-2 */
          Style.PaintHline(Style, GdkWindow, stateType, _rect, this, null,
                           (leftMargin - offset), /* X1 */
                           ((Allocation.Width - rightMargin) + offset), /* X2 */
                           y); /* Y */
        } /* end for each quarter draw hour demarcs */
        if (CalendarEvents != null)
          foreach(CalendarDay day in dates)
            day.Paint(Style, this, increment, CalendarEvents);
      } /* end if is drawable */
    }

    protected virtual void DoFreeMotion(double x, double y, DateTime w)
    {
    } /* end DoFreeMotion */

    protected virtual void DoLeftPress(double x, double y, DateTime w)
    {
      if (SelectEvent(x, y))
      {
        if (selectedEvent.Draggable)
        {
          Console.WriteLine("  Selected event {0} - {1} : {2}", 
            selectedEvent.Start, 
            selectedEvent.End, 
            selectedEvent.Title);
          dragMode = DragMode.Appointment;
        } else 
          {
            selectedEvent = null;
            dragMode = DragMode.NoDrag;
            Console.WriteLine("Drag supresses by permisions");
          }
      }
    } /* end DoLeftPress */
    
    protected virtual void DoLeftMotion(double x, double y, DateTime _w)
    {
      double duration;
      
      if (selectedEvent == null)
        return;
        
      Console.WriteLine("Dragging event {0} - {1} : {2}", 
        selectedEvent.Start, 
        selectedEvent.End, 
        selectedEvent.Title);
      duration = selectedEvent.End.Subtract(selectedEvent.Start).TotalSeconds;
      selectedEvent.Start = _w;
      selectedEvent.End = _w.AddSeconds(duration);
      Repaint();
    } /* end DoLeftMotion */

    protected virtual void DoLeftRelease(double _x, double _y, DateTime _w)
    {
      double duration;
      
      if (selectedEvent == null)
        return;
      
      Console.WriteLine("Releasing event {0} - {1} : {2}", 
        selectedEvent.Start, 
        selectedEvent.End, 
        selectedEvent.Title);
      duration = selectedEvent.End.Subtract(selectedEvent.Start).TotalSeconds;
      selectedEvent.Start = _w;
      selectedEvent.End = _w.AddSeconds(duration);
      if (OnEventDrop != null)
        OnEventDrop(selectedEvent, _w);
      Repaint();
      dragMode = DragMode.NoDrag;
      selectedEvent = null;
    } /* end DoLeftRelease */

    protected virtual void DoMiddlePress(double x, double y, DateTime w)
    {
    }
    
    protected virtual void DoMiddleMotion(double x, double y, DateTime w)
    {
    }
    
    protected virtual void DoMiddleRelease(double x, double y, DateTime w)
    {
    }

    protected virtual void DoRightPress(double x, double y, DateTime w)
    {
    }
    
    protected virtual void DoRightMotion(double x, double y, DateTime w)
    {
    }
    
    protected virtual void DoRightRelease(double x, double y, DateTime w)
    {
      if (SelectEvent(x, y))
      {
        if (OnEventContext != null)
          OnEventContext(selectedEvent);
      } else
        {
          if (OnStripContext != null)
            OnStripContext(this, w);
        }
    } /* end DoRightRelease */
  } /* end DayStrip */
}